Odomknite pokročilé spracovanie videa v prehliadači. Naučte sa priamo pristupovať a manipulovať so surovými dátami rovín VideoFrame pomocou WebCodecs API pre vlastné efekty a analýzu.
Prístup k rovinám VideoFrame cez WebCodecs: Hĺbkový pohľad na manipuláciu so surovými video dátami
Po celé roky sa vysokovýkonné spracovanie videa vo webovom prehliadači zdalo byť vzdialeným snom. Vývojári boli často obmedzení limitáciami elementu <video> a 2D Canvas API, ktoré, hoci boli výkonné, prinášali problémy s výkonom a obmedzený prístup k základným surovým video dátam. Príchod WebCodecs API zásadne zmenil túto situáciu, poskytujúc nízkoúrovňový prístup k vstavaným mediálnym kodekom prehliadača. Jednou z jeho najrevolučnejších funkcií je schopnosť priamo pristupovať a manipulovať so surovými dátami jednotlivých video snímok prostredníctvom objektu VideoFrame.
Tento článok je komplexným sprievodcom pre vývojárov, ktorí sa chcú posunúť za hranice jednoduchého prehrávania videa. Preskúmame zložitosti prístupu k rovinám VideoFrame, demystifikujeme pojmy ako farebné priestory a rozloženie pamäte a poskytneme praktické príklady, ktoré vám umožnia vytvárať novú generáciu video aplikácií v prehliadači, od filtrov v reálnom čase až po sofistikované úlohy počítačového videnia.
Predpoklady
Aby ste z tohto sprievodcu vyťažili maximum, mali by ste mať solídne znalosti:
- Moderný JavaScript: Vrátane asynchrónneho programovania (
async/await, Promises). - Základné koncepty videa: Znalosť pojmov ako snímky, rozlíšenie a kodeky je užitočná.
- API prehliadača: Skúsenosti s API ako Canvas 2D alebo WebGL budú výhodou, ale nie sú striktne vyžadované.
Pochopenie video snímok, farebných priestorov a rovín
Predtým, ako sa ponoríme do API, musíme si najprv vytvoriť pevný mentálny model toho, ako dáta video snímky v skutočnosti vyzerajú. Digitálne video je sekvencia statických obrázkov, čiže snímok. Každá snímka je mriežka pixelov a každý pixel má farbu. Spôsob, akým je táto farba uložená, je definovaný farebným priestorom a formátom pixelov.
RGBA: Rodný jazyk webu
Väčšina webových vývojárov pozná farebný model RGBA. Každý pixel je reprezentovaný štyrmi zložkami: červená (Red), zelená (Green), modrá (Blue) a alfa (priehľadnosť). Dáta sú zvyčajne uložené prekladane (interleaved) v pamäti, čo znamená, že hodnoty R, G, B a A pre jeden pixel sú uložené za sebou:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
V tomto modeli je celý obrázok uložený v jednom súvislom bloku pamäte. Môžeme si to predstaviť ako jednu „rovinu“ dát.
YUV: Jazyk kompresie videa
Video kodeky však zriedka pracujú priamo s RGBA. Uprednostňujú farebné priestory YUV (alebo presnejšie Y'CbCr). Tento model rozdeľuje obrazové informácie na:
- Y (Luma): Informácia o jase alebo odtieňoch sivej. Ľudské oko je najcitlivejšie na zmeny jasu.
- U (Cb) a V (Cr): Informácie o farbonosnosti (chrominancia) alebo farebnom rozdiele. Ľudské oko je menej citlivé na farebné detaily ako na detaily jasu.
Toto oddelenie je kľúčom k efektívnej kompresii. Znížením rozlíšenia zložiek U a V – technikou nazývanou chroma subsampling (farebné podvzorkovanie) – môžeme výrazne zmenšiť veľkosť súboru s minimálnou vnímateľnou stratou kvality. To vedie k planárnym formátom pixelov, kde sú zložky Y, U a V uložené v samostatných pamäťových blokoch, čiže „rovinách“.
Bežným formátom je I420 (typ YUV 4:2:0), kde na každý blok pixelov 2x2 pripadajú štyri vzorky Y, ale iba jedna vzorka U a jedna vzorka V. To znamená, že roviny U a V majú polovičnú šírku a polovičnú výšku roviny Y.
Pochopenie tohto rozdielu je kľúčové, pretože WebCodecs vám poskytuje priamy prístup práve k týmto rovinám, presne tak, ako ich poskytuje dekodér.
Objekt VideoFrame: Vaša brána k pixelovým dátam
Ústredným prvkom tejto skladačky je objekt VideoFrame. Reprezentuje jednu snímku videa a obsahuje nielen pixelové dáta, ale aj dôležité metadáta.
Kľúčové vlastnosti VideoFrame
format: Reťazec označujúci formát pixelov (napr. 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Plné rozmery snímky tak, ako je uložená v pamäti, vrátane akéhokoľvek výplňového priestoru (padding) vyžadovaného kodekom.displayWidth/displayHeight: Rozmery, ktoré by sa mali použiť na zobrazenie snímky.timestamp: Prezentačná časová značka snímky v mikrosekundách.duration: Trvanie snímky v mikrosekundách.
Magická metóda: copyTo()
Hlavnou metódou na prístup k surovým pixelovým dátam je videoFrame.copyTo(destination, options). Táto asynchrónna metóda kopíruje dáta rovín snímky do vami poskytnutého buffera.
destination:ArrayBufferalebo typované pole (akoUint8Array) dostatočne veľké na uloženie dát.options: Objekt, ktorý špecifikuje, ktoré roviny sa majú kopírovať a aké je ich rozloženie v pamäti. Ak je vynechaný, skopírujú sa všetky roviny do jedného súvislého buffera.
Metóda vracia Promise, ktorý sa vyrieši s poľom objektov PlaneLayout, jeden pre každú rovinu v snímke. Každý objekt PlaneLayout obsahuje dve kľúčové informácie:
offset: Bytový posun (offset), kde začínajú dáta tejto roviny v cieľovom bufferi.stride: Počet bytov medzi začiatkom jedného riadku pixelov a začiatkom nasledujúceho riadku pre danú rovinu.
Kritický koncept: Stride vs. šírka
Toto je jeden z najčastejších zdrojov zmätku pre vývojárov, ktorí sú noví v nízkoúrovňovom grafickom programovaní. Nemôžete predpokladať, že každý riadok pixelových dát je tesne za sebou.
- Šírka (Width) je počet pixelov v riadku obrázka.
- Stride (tiež nazývaný pitch alebo line step) je počet bytov v pamäti od začiatku jedného riadku po začiatok nasledujúceho.
Často bude stride väčší ako šírka * bajty_na_pixel. Je to preto, lebo pamäť je často doplnená (padded) tak, aby bola zarovnaná s hardvérovými hranicami (napr. 32- alebo 64-bajtovými hranicami) pre rýchlejšie spracovanie CPU alebo GPU. Na výpočet adresy pixelu v konkrétnom riadku musíte vždy použiť stride.
Ignorovanie stride povedie k zošikmeným alebo skresleným obrázkom a nesprávnemu prístupu k dátam.
Praktický príklad 1: Prístup a zobrazenie roviny v odtieňoch sivej
Začnime jednoduchým, ale silným príkladom. Väčšina videa na webe je kódovaná vo formáte YUV, ako je I420. Rovina 'Y' je v podstate kompletná reprezentácia obrazu v odtieňoch sivej. Môžeme extrahovať len túto rovinu a vykresliť ju na canvas.
async function displayGrayscale(videoFrame) {
// Predpokladáme, že videoFrame je vo formáte YUV ako 'I420' alebo 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Tento príklad vyžaduje planárny formát YUV 4:2:0.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Rovina Y je vždy prvá.
// Vytvoríme buffer na uloženie dát iba z roviny Y.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Skopírujeme rovinu Y do nášho buffera.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Teraz yPlaneData obsahuje surové pixely v odtieňoch sivej.
// Musíme to vykresliť. Vytvoríme RGBA buffer pre canvas.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Prejdeme pixely canvasu a naplníme ich dátami z roviny Y.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Dôležité: Na nájdenie správneho zdrojového indexu použite stride!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Vypočítame cieľový index v RGBA bufferi ImageData.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Červená
imageData.data[rgbaIndex + 1] = luma; // Zelená
imageData.data[rgbaIndex + 2] = luma; // Modrá
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRITICKÉ: Vždy zatvorte VideoFrame, aby sa uvoľnila jeho pamäť.
videoFrame.close();
}
Tento príklad zdôrazňuje niekoľko kľúčových krokov: identifikáciu správneho rozloženia roviny, alokáciu cieľového buffera, použitie copyTo na extrakciu dát a správne iterovanie cez dáta pomocou stride na vytvorenie nového obrázka.
Praktický príklad 2: Priama manipulácia (Sépia filter)
Teraz vykonajme priamu manipuláciu s dátami. Sépia filter je klasický efekt, ktorý sa ľahko implementuje. Pre tento príklad je jednoduchšie pracovať s RGBA snímkou, ktorú môžete získať z canvasu alebo WebGL kontextu.
async function applySepiaFilter(videoFrame) {
// Tento príklad predpokladá, že vstupná snímka je 'RGBA' alebo 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Príklad so sépia filtrom vyžaduje RGBA snímku.');
videoFrame.close();
return null;
}
// Alokujeme buffer na uloženie pixelových dát.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA je jedna rovina
// Teraz zmanipulujeme dáta v bufferi.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 bajty na pixel (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) zostáva nezmenená.
}
}
// Vytvoríme *nový* VideoFrame s upravenými dátami.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Nezabudnite zatvoriť pôvodnú snímku!
videoFrame.close();
return newFrame;
}
Toto demonštruje kompletný cyklus čítania-úpravy-zápisu: skopírujeme dáta, prejdeme ich v cykle pomocou stride, aplikujeme matematickú transformáciu na každý pixel a vytvoríme nový VideoFrame s výslednými dátami. Táto nová snímka môže byť potom vykreslená na canvas, odoslaná do VideoEncoder alebo postúpená ďalšiemu kroku spracovania.
Na výkone záleží: JavaScript vs. WebAssembly (WASM)
Iterovanie cez milióny pixelov pre každú snímku (snímka 1080p má viac ako 2 milióny pixelov, alebo 8 miliónov dátových bodov v RGBA) v JavaScripte môže byť pomalé. Hoci sú moderné JS enginy neuveriteľne rýchle, pri spracovaní videa s vysokým rozlíšením (HD, 4K) v reálnom čase môže tento prístup ľahko preťažiť hlavné vlákno, čo vedie k trhanému používateľskému zážitku.
Tu sa stáva WebAssembly (WASM) nevyhnutným nástrojom. WASM vám umožňuje spúšťať kód napísaný v jazykoch ako C++, Rust alebo Go takmer natívnou rýchlosťou priamo v prehliadači. Pracovný postup pre spracovanie videa vyzerá takto:
- V JavaScripte: Použite
videoFrame.copyTo()na získanie surových pixelových dát doArrayBuffer. - Odovzdanie do WASM: Odovzdajte referenciu na tento buffer do vášho skompilovaného WASM modulu. Toto je veľmi rýchla operácia, pretože nezahŕňa kopírovanie dát.
- Vo WASM (C++/Rust): Spustite svoje vysoko optimalizované algoritmy na spracovanie obrazu priamo na pamäťovom bufferi. Je to o niekoľko rádov rýchlejšie ako JavaScriptová slučka.
- Návrat do JavaScriptu: Keď WASM skončí, riadenie sa vráti do JavaScriptu. Potom môžete použiť upravený buffer na vytvorenie nového
VideoFrame.
Pre akúkoľvek serióznu aplikáciu na manipuláciu s videom v reálnom čase – ako sú virtuálne pozadia, detekcia objektov alebo komplexné filtre – využitie WebAssembly nie je len možnosťou, ale nevyhnutnosťou.
Spracovanie rôznych formátov pixelov (napr. I420, NV12)
Hoci je RGBA jednoduché, najčastejšie budete dostávať snímky v planárnych YUV formátoch z VideoDecoder. Pozrime sa, ako spracovať plne planárny formát ako I420.
VideoFrame vo formáte I420 bude mať vo svojom poli layout tri deskriptory rozloženia:
layout[0]: Rovina Y (luma). Rozmery súcodedWidthxcodedHeight.layout[1]: Rovina U (chroma). Rozmery súcodedWidth/2xcodedHeight/2.layout[2]: Rovina V (chroma). Rozmery súcodedWidth/2xcodedHeight/2.
Takto by ste skopírovali všetky tri roviny do jedného buffera:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts je pole 3 objektov PlaneLayout
console.log('Y Plane Layout:', layouts[0]); // { offset: 0, stride: ... }
console.log('U Plane Layout:', layouts[1]); // { offset: ..., stride: ... }
console.log('V Plane Layout:', layouts[2]); // { offset: ..., stride: ... }
// Teraz môžete pristupovať ku každej rovine v bufferi `allPlanesData`
// pomocou jej špecifického offsetu a stride.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Všimnite si, že rozmery chroma sú polovičné!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Accessed Y plane size:', yPlaneView.byteLength);
console.log('Accessed U plane size:', uPlaneView.byteLength);
videoFrame.close();
}
Ďalším bežným formátom je NV12, ktorý je semi-planárny. Má dve roviny: jednu pre Y a druhú rovinu, kde sú hodnoty U a V prekladané (napr. [U1, V1, U2, V2, ...]). WebCodecs API to spracováva transparentne; VideoFrame vo formáte NV12 bude mať vo svojom poli layout jednoducho dve rozloženia.
Výzvy a osvedčené postupy
Práca na tejto nízkej úrovni je mocná, ale prináša so sebou zodpovednosť.
Správa pamäte je prvoradá
VideoFrame drží značné množstvo pamäte, ktorá je často spravovaná mimo haldy JavaScriptového garbage collectora. Ak túto pamäť explicitne neuvoľníte, spôsobíte únik pamäte (memory leak), ktorý môže zhodiť kartu prehliadača.
Vždy, vždy volajte videoFrame.close(), keď skončíte prácu so snímkou.
Asynchrónna povaha
Všetok prístup k dátam je asynchrónny. Architektúra vašej aplikácie musí správne zvládnuť tok Promises a async/await, aby sa predišlo race conditions a zabezpečil sa plynulý proces spracovania.
Kompatibilita prehliadačov
WebCodecs je moderné API. Hoci je podporované vo všetkých hlavných prehliadačoch, vždy skontrolujte jeho dostupnosť a buďte si vedomí akýchkoľvek implementačných detailov alebo obmedzení špecifických pre dodávateľa. Pred pokusom o použitie API použite detekciu funkcií (feature detection).
Záver: Nová hranica pre webové video
Schopnosť priamo pristupovať a manipulovať so surovými dátami rovín VideoFrame prostredníctvom WebCodecs API je zmenou paradigmy pre webové mediálne aplikácie. Odstraňuje čiernu skrinku elementu <video> a dáva vývojárom granulárnu kontrolu, ktorá bola predtým vyhradená pre natívne aplikácie.
Pochopením základov rozloženia video pamäte – rovín, stride a farebných formátov – a využitím sily WebAssembly pre operácie kritické na výkon, môžete teraz vytvárať neuveriteľne sofistikované nástroje na spracovanie videa priamo v prehliadači. Od farebných úprav v reálnom čase a vlastných vizuálnych efektov až po klientské strojové učenie a video analýzu, možnosti sú obrovské. Éra vysokovýkonného, nízkoúrovňového videa na webe sa skutočne začala.